package expo.modules.backgroundtask

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.Operation
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.google.common.util.concurrent.ListenableFuture
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.time.Duration
import java.util.concurrent.TimeUnit

object BackgroundTaskScheduler {
  // Default interval
  const val DEFAULT_INTERVAL_MINUTES = 60L * 24L // Once every day

  // Unique identifier (generated by us) to identify the worker
  private const val WORKER_IDENTIFIER = "EXPO_BACKGROUND_WORKER"

  // Log tag
  private val TAG = BackgroundTaskScheduler::class.java.simpleName

  // Number of active task consumers
  private var numberOfRegisteredTasksOfThisType = 0

  // Interval
  private var intervalMinutes: Long = DEFAULT_INTERVAL_MINUTES

  // Tracks whether in foreground
  var inForeground: Boolean = false

  /**
   * Call when a task is registered
   */
  fun registerTask(context: Context, intervalMinutes: Long) {
    numberOfRegisteredTasksOfThisType += 1
    this.intervalMinutes = intervalMinutes

    if (numberOfRegisteredTasksOfThisType == 1) {
      runBlocking {
        scheduleWorker(context, context.packageName)
      }
    }
  }

  /**
   * Call when a task is unregistered
   */
  fun unregisterTask(context: Context) {
    numberOfRegisteredTasksOfThisType -= 1

    if (numberOfRegisteredTasksOfThisType == 0) {
      runBlocking {
        stopWorker(context)
      }
    }
  }

  /**
   * Schedules the worker task to run. The worker should run periodically.
   */
  private suspend fun scheduleWorker(context: Context, appScopeKey: String, cancelExisting: Boolean = true, overriddenIntervalMinutes: Long = intervalMinutes): Boolean {
    if (numberOfRegisteredTasksOfThisType == 0) {
      Log.d(TAG, "Will not enqueue worker. No registered tasks to run.")
      return false
    }

    // Stop the current worker (if any)
    if (cancelExisting) {
      stopWorker(context)
    }

    Log.d(TAG, "Enqueuing worker with identifier $WORKER_IDENTIFIER and '$overriddenIntervalMinutes' minutes delay.")

    // Build the work request
    val data = Data.Builder()
      .putString("appScopeKey", appScopeKey)
      .build()
    val constraints = Constraints.Builder()
      .setRequiredNetworkType(NetworkType.CONNECTED)
      .build()

    // Get Work manager
    val workManager = WorkManager.getInstance(context)

    // We have two different paths here, since on Android 14-15 we need to use a periodic request
    // builder since the OneTimeWorkRequest doesn't support setting initial delay (which is how we
    // control executing a task periodically - we spawn a One-time work request when backgrounding,
    // and when this is done we spawn a new one. This makes it a lot easier to debug since we can
    // use adb to spawn jobs whenever we want)
    // The following is the path we have to follow on  Android.O and later:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      // Create the work request
      val builder = OneTimeWorkRequestBuilder<BackgroundTaskWork>()
        .setInputData(data)
        .setConstraints(constraints)

      // Add minimum interval here as well so that the work doesn't start immediately
      builder.setInitialDelay(Duration.ofMinutes(overriddenIntervalMinutes))

      // Create work request
      val workRequest = builder.build()

      // Enqueue the work
      return try {
        workManager.enqueueUniqueWork(
          WORKER_IDENTIFIER,
          // This is where we decide if we should cancel or replace the task - cancelling is done
          // when spawning the first task, while appending is when we spawn from a running task
          // to set up the next periodic run of the task
          if (cancelExisting) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.APPEND,
          workRequest
        ).await()

        true
      } catch (e: Exception) {
        Log.e(TAG, "Worker failed to start with error " + e.message)
        false
      }
    } else {
      val builder = PeriodicWorkRequestBuilder<BackgroundTaskWork>(
        repeatIntervalTimeUnit = TimeUnit.MINUTES,
        repeatInterval = intervalMinutes
      )

      // Create work request
      val workRequest = builder.build()

      // Enqueue the work
      return try {
        workManager.enqueueUniquePeriodicWork(
          WORKER_IDENTIFIER,
          ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
          workRequest
        ).await()

        true
      } catch (e: Exception) {
        Log.e(TAG, "Worker failed to start with error " + e.message)
        false
      }
    }
  }

  /**
   * Cancels the worker task
   */
  private suspend fun stopWorker(context: Context): Boolean {
    if (!isWorkerRunning(context)) {
      return false
    }

    Log.d(TAG, "Cancelling worker with identifier $WORKER_IDENTIFIER")

    // Stop our main worker
    val workManager = WorkManager.getInstance(context)
    return try {
      workManager.cancelUniqueWork(WORKER_IDENTIFIER).await()
    } catch (e: Exception) {
      Log.d(TAG, "Stopping worker failed with error " + e.message)
      false
    }
  }

  /**
   * Returns true if the worker task is pending
   */
  private suspend fun isWorkerRunning(context: Context): Boolean {
    val workInfo = getWorkerInfo(context)
    return workInfo?.state == WorkInfo.State.RUNNING ||
      workInfo?.state == WorkInfo.State.ENQUEUED
  }

  /**
   * Runs tasks with the given appScopeKey
   */
  suspend fun runTasks(context: Context, appScopeKey: String) {
    // Get task service
    val taskService = TaskServiceProviderHelper.getTaskServiceImpl(context)
      ?: throw MissingTaskServiceException()

    Log.d(TAG, "runTasks: $appScopeKey")

    // Get all task consumers
    val consumers = taskService.getTaskConsumers(appScopeKey)
    Log.d(TAG, "runTasks: number of consumers ${consumers.size}")

    if (consumers.isEmpty()) {
      return
    }

    // Make sure we're in the background before running a task
    if (inForeground) {
      // Schedule task in an hour (or at least minimumInterval) - the app is foregrounded and
      // we don't want to run anything to avoid performance issues.
      Log.d(TAG, "runTasks: App is in the foreground")
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        scheduleWorker(context, appScopeKey, false, 60L.coerceAtMost(intervalMinutes))
        Log.d(TAG, "runTasks: Scheduled new worker in $intervalMinutes minutes")
      }
      return
    }

    val tasks = consumers.filterIsInstance<BackgroundTaskConsumer>()
      .map { bgTaskConsumer ->
        Log.d(TAG, "runTasks: executing tasks for consumer of type ${bgTaskConsumer.taskType()}")
        val taskCompletion = CompletableDeferred<Unit>()
        try {
          bgTaskConsumer.executeTask {
            Log.d(TAG, "Task successfully finished")
            taskCompletion.complete(Unit)
          }
        } catch (e: Exception) {
          Log.e(TAG, "Task failed: ${e.message}")
        }
        taskCompletion
      }
    // Await all tasks to complete
    tasks.awaitAll()

    // Schedule next task
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      scheduleWorker(context, appScopeKey, false)
    }
  }

  /**
   * Returns the worker info object from the WorkManager if the worker has been
   * registered, otherwise returns null
   */
  private suspend fun getWorkerInfo(context: Context): WorkInfo? {
    // Get work manager
    val workManager = WorkManager.getInstance(context)

    return try {
      val workInfos = workManager.getWorkInfosForUniqueWork(WORKER_IDENTIFIER).await()
      return workInfos.firstOrNull()
    } catch (e: Exception) {
      Log.d(TAG, "Calling getWorkInfosForUniqueWork failed with error " + e.message)
      null
    }
  }

  /**
   * Helper function for calling functions returning an Operation
   */
  private suspend fun Operation.await(): Boolean = withContext(Dispatchers.IO) {
    result.get()
    true
  }

  /**
   * Helper function for calling functions returning a ListenableFuture
   */
  private suspend fun <T> ListenableFuture<T>.await(): T = withContext(Dispatchers.IO) {
    try {
      get()
    } catch (e: CancellationException) {
      cancel(true)
      throw e
    } catch (e: Exception) {
      throw e
    }
  }
}
